#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <strstream>
#include <vector>
#include <GL/freeglut.h>

#include <Common/iruntime.h>
#include <Geometry/cubic_bspline_curve.h>

namespace shermit {
	template<typename T>
	void printMatrix(const T* A, int m, int n, int lda, const char* name=nullptr) {
		if (name)
			std::cout << "Print Matrix " << name << std::endl;
		else std::cout << "Print Matrix" << std::endl;
		for (int i = 0; i < m; ++i) {
			for (int j = 0; j < n; ++j) {
				std::cout << A[j * lda + i] << " ";
			}
			std::cout << ";"<<std::endl;
		}
	}

	template<typename Float_t>
	std::vector<nv::vec2<Float_t>> readVecs(const std::string& filename) {
		std::ifstream f(filename);
		std::string line;
		std::getline(f, line);
		int num_pts;
		sscanf_s(line.c_str(), "%d\n", &num_pts);
		std::vector<nv::vec2<Float_t>> pts(num_pts);
		for (int i = 0; i < num_pts && f && !f.eof(); ++i) {
			std::getline(f, line);
			if (f.bad())
			{
				std::cerr << "  Warning! Could not read file properly!\n";
				abort();
			}
			std::istrstream reader(line.c_str());
			Float_t x, y;
			reader >> x >> y;
			pts[i] = { x, y };
		}
		f.close();
		return pts;
	}

	class BSplineCurveViewer : public IRuntimeModule {
	public:
		~BSplineCurveViewer() { curve.deallocate(); }
		BSplineCurveViewer(const BSplineCurveViewer&) = delete;
		BSplineCurveViewer& operator=(const BSplineCurveViewer&) = delete;

		static BSplineCurveViewer& getInstance() {
			static BSplineCurveViewer instance;
			return instance;
		}

		void setInitWindowTitle(const char* title) {
			this->title = std::string(title);
		}

		void setWindowSize(int w, int h) { _w = w; _h = h; }

		void setSampleFrequency(int fs) {
			this->fs = fs;
		}

		template<DeviceType _dev>
		void setBSplineCurve(const CubicBSplineCurve<double, _dev>& curve) {
			this->curve.setControlPts(curve.getControlPts(), curve.nControlPts(), _dev);
		}

		void scale(double max_height = 500) {
			this->max_height = max_height;
			// translate and scale the curve
			double ymax = -1;
			const auto* pts = curve.getControlPts();
			for (int i = 0; i < curve.nControlPts(); ++i) {
				if (pts[i].y > ymax) {
					ymax = pts[i].y;
				}
			}
			for (int i = 0; i < dpts.size(); ++i) {
				if (dpts[i].y > ymax) {
					ymax = dpts[i].y;
				}
			}
			assert(ymax > 0);
			double scale_factor = max_height / ymax;
			for (int i = 0; i < curve.nControlPts(); ++i) {
				auto pt = pts[i] * scale_factor;
				curve.setControlPt(i, pt);
			}

			for (int i = 0; i < dpts.size(); ++i) {
				dpts[i] = dpts[i] * scale_factor;
			}
		}

		void setExtraParams(const double* t, int size) {
			ts.clear();
			ts.reserve(size);
			ts.assign(t, t + size);
		}

		void setDataPoints(const nv::vec2<double>* dpts, int size) {
			this->dpts.assign(dpts, dpts + size);
		}

		int initialize(int argc, char** argv) override {
			assert(curve.nControlPts());
			if (ready) return true;
			// initialise glut
			glutInit(&argc, argv);

			// request a depth buffer, RGBA display mode, and we want double buffering
			glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);

			// set the initial window size
			glutInitWindowSize(_w, _h);

			// create the window
			glutCreateWindow(title.c_str());

			// set the function to use to draw our scene
			glutDisplayFunc(BSplineCurveViewer::onDraw);

			// set the function to handle changes in screen size
			glutReshapeFunc(BSplineCurveViewer::onReshape);

			glutIdleFunc(BSplineCurveViewer::idleFunc);

			// set the function to be called when we exit
			atexit(BSplineCurveViewer::onExit);

			ready = true;
			return ready;
		}

		void tick() override {
			glutMainLoop();
		}

		void finalize() override { ready = false; }
	protected:
		std::string title;
		int max_height;
		int _w, _h;
		bool ready;  // ready for runtime exec
		int fs;      // sample frequency, i.e. fs samples in range [t, t+1)
		CubicBSplineCurve<double, DeviceType::CPU> curve;
		std::vector<double> ts;  // additional curve params
		std::vector<nv::vec2<double>> dpts;
	private:
		BSplineCurveViewer() :_w(1280), _h(720), max_height(500),
			ready(false), fs(20),
			title("default title") {}

		static void onDraw() {
			auto& viewer = BSplineCurveViewer::getInstance();
			auto& curve = viewer.curve;
			const nv::vec2<double>* pts = curve.getControlPts();
			double mh = viewer.max_height;

			// clear the screen & depth buffer
			glClear(GL_COLOR_BUFFER_BIT);

			// clear the previous transform
			glLoadIdentity();

			glPointSize(2);

			// draw control points
			glColor3f(0, 1, 0);
			glBegin(GL_POINTS);
			for (int i = 0; i < curve.nControlPts(); ++i) {
				//printf("%lf %lf\n", pts[i].x, pts[i].y);
				glVertex2d(pts[i].x,mh-pts[i].y);
			}
			glEnd();
			/*
			// draw curve hull
			glColor3f(0.3, 0, 0.5);
			glBegin(GL_LINE_STRIP);
			for (int i = 0; i != curve.nControlPts(); ++i) {
				glVertex2d(pts[i].x, mh - pts[i].y);
			}
			glEnd();
			*/
			glColor3f(0, 1, 0);
			// begin drawing our curve
			glBegin(GL_LINE_STRIP);
			double step = 1.0/viewer.fs;
			for (double t = curve.tMin(); t < curve.tMax(); t+=step) {
				auto pt = curve.at(t);
				glVertex2d(pt.x, mh - pt.y);
			}
			glEnd();

			// draw additional curve points
			glColor3f(1, 0.412, 0.706);  // hot pink
			glBegin(GL_POINTS);
			for (int i = 0; i < viewer.ts.size(); ++i) {
				double t = viewer.ts[i];
				auto pt = curve.at(t);
				glVertex2d(pt.x, mh - pt.y);
			}
			for (int i = 0; i < viewer.dpts.size(); ++i) {
				//printf("%lf %lf\n", pts[i].x, pts[i].y);
				auto pt = viewer.dpts[i];
				glVertex2d(pt.x, mh - pt.y);
			}
			glEnd();

			// currently we've been drawing to the back buffer, we need
			// to swap the back buffer with the front one to make the image visible
			glutSwapBuffers();
		}

		static void onReshape(int w, int h) {
			// prevents division by zero when minimising window
			if (h == 0)
				h = 1;

			// set the drawable region of the window
			glViewport(0, 0, w, h);
			auto& viewer = BSplineCurveViewer::getInstance();
			viewer.setWindowSize(w, h);
			double mh = viewer.max_height;

			// set up the projection matrix
			glMatrixMode(GL_PROJECTION);

			glLoadIdentity();

			// just use a perspective projection

			//gluPerspective(45,(float)w/h,0.1,100);
			if (w <= h)
				glOrtho(0, mh* (GLdouble)w / (GLdouble)h, 0, mh, 0.0, 100.0);
			else
				glOrtho(0, mh* (GLdouble)w / (GLdouble)h, 0, mh, 0.0, 100.0);

			// go back to modelview matrix so we can move the objects about
			glMatrixMode(GL_MODELVIEW);
			glLoadIdentity();
		}

		static void idleFunc() { glutPostRedisplay(); }

		static void onExit() {}
	};

}

